Repository navigation
feat(core): supply the Grate bridge's index — the read stops being forged - #127
Merged
Merged
Conversation
`mfAssocFunction1` sampled the outer rebuild once and broadcast the result, so `grate ∘ iso` collapsed every position onto index 0 — silently, behind a matrix cell that pins typing only. `MultiFocus.tuple`, `representable` and `apply` all tabulate, so the sampled value was wrong for all three. - `Function1BroadcastOptic[S, T, A, B, X0]` is the witness for the one inner kind whose write has exactly one value to build; the kernel writes it per index (`broadcastFrom`) and reads the inner's bundle per index for every inner. - `Z = Xo`: the outer's leftover is threaded through the composition instead of being forged, so the outer's `from` receives what its own `to` produced. - `unobserved[A]` names the two remaining stand-ins (an uninhabited index type, an inner leftover the kernel cannot produce); the class's own `from` is the only index site left and is guarded by the constant-bundle contract. - `MultiFocusFunction1Spec`: positional read/modify/replace across all three Grate factories, the bundle-inner diagonal read, and the previously mis-titled `.andThen` block fixed. - `GrateShapeSpec`: the Naperian sub-shape's 23-cell footprint (6 composing / 18 structural voids), compile-pinned. - QA page: generated Grate sub-table, a legend that states ✓ is a *typing* claim, and carrier-level caveats; `optics.md` + `multifocus.md` cross-link it and the stale `Z = (Xo, Xi)` claim is corrected.
…rged The `Direct → MultiFocus[Function1[X0, *]]` bridge's product must read a bundle it did not build (`Function1BroadcastOptic.from`), and its carrier stores only `Unit`. Baseline 81a53d3d left that read at a `null.asInstanceOf[X0]` sentinel guarded by the constant-bundle contract. - `data.RepresentativeIndex[X0]` — the witness: a real index, with canonical instances for the index types the Grate factories fix with one (`Int` → 0 for `tuple` and `apply` over `Function1[Int, *]`, `Boolean` → false, `Unit` → ()), singletons via `ValueOf`, and `at(i)` for everything else. - `Function1BroadcastOptic` stores `at` and reads there; the bridge's given takes the witness, so `iso.andThen(grate)` stays import-free wherever an instance exists and is REFUSED for an uninhabited index type (there is no index to witness, so the read that has no answer is refused rather than forged). - `unobserved` keeps its two remaining sites, both existential leftovers the write path discards; its docstring now states that neither is an index — an index witness cannot stand in for per-position data. - Spec (13 blocks): the witness read is observable (a varying bundle at 0 vs 1 gives -1000 vs -990, white-box), the shipped path is witness-invariant (round trip + `modify`), `grate ∘ iso` stays positional under a non-canonical witness, an algebraic index bridges via a one-line `given`, an unwitnessed index does not bridge (`typeChecks`), the shipped instances name real values, and the Boolean-indexed cell resolves off the companion. - Docs: `multifocus.md` bridge row + composition-limits paragraph, the generated QA grate legend (script + page kept identical), the `core` row of the agent guide, and a CHANGELOG entry. Design note, measurements and the alternative supplies (cats' `Representable` has no representative index; `ValueOf` does not cover `Int`/`Boolean`) in docs/research/2026-09-30-grate-witness-index.md. Also formats `GrateShapeSpec` (the baseline commit left it non-scalafmt-clean). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
🚀 Cloudflare Pages preview for https://b8292859.cats-eo-docs.pages.dev Branch alias: https://feat-grate-witness-index.cats-eo-docs.pages.dev Built from commit |
Contributor
Benchmark A/BAllocation (B/op) — authoritative
442 more benchmarks
Timing (ns/op) — directional only, same-VM but shared runner
base_sha: |
kryptt
added a commit
that referenced
this pull request
Oct 7, 2026
* feat(core): express grate constancy in the bundle, on top of #127 Rebased onto `52671355` (the optic-class answer: `Z = Xo`, `Function1BroadcastOptic`, `RepresentativeIndex`, the positional write). That commit and this one answer different questions, and this rebase keeps upstream's machinery — and upstream's *idiom* — wherever it is better: - **kept** `Function1BroadcastOptic` as the kernel's per-index write hook (one value per position, no bundle read it did not build) and the bridge's `RepresentativeIndex[X0]` witness; - **added** the sum: the focus half is `F[A] | Broadcast[F, A]` (both cases `private[eo]`), with `MultiFocus.broadcast` / `.broadcast` as the public handles and `.foci` total for both (the index-free case carries its `F[A]` image); - **added** the kernel's remaining cases: a composite inner is written per position with the leftover *its own* read produced (upstream forges it, so a `fromLensF` inner NPEs), and a tabulating ∘ tabulating composite writes the exact inverse of its diagonal read. That last one needs `C =:= D`, which is a property of the *optic* — so it rides on the optic as `Optic.SameFocus` (mixed into every `andThen` composite from the call site's `summonFrom`, and into the monomorphic factories `representable` / `tuple` / `apply` with `Some(…)`), and the kernel reads it off the `inner` it is already handed. `AssociativeFunctor` and the composition algebra are untouched (zero diff vs upstream). - **deleted `unobserved`** — no write needs a stand-in any more. Its three historical sites are fixed at the source: the kernel records the inner leftovers its read observed, the bridge takes a real index witness, and the shape-collapsing `collectList` now hands the write the leftover its own read produced (`o.from((bundle.context, List(agg(bundle.foci))))`), which fixes a live NPE / `MatchError` on Lens / Prism / Optional provenances (probed before the fix). New coverage: `tests/.../MultiFocusCrossFamilySpec.scala` is the runtime twin of `CompositionMatrixSpec` — nine property blocks over every inbound provenance (generic factory, polymorphic factory, Iso/Lens/Prism/Optional bridges, a composite of two shipped optics) through `modify` / `replace` / `collectMap` / `collectWith` / `collectList` / `foldMap` / `headOption` / `length`, plus both composition directions across Iso / Lens / Prism / Optional with independently computed expectations. The collapsing aggregate is asserted only where the optic's leftover is count-agnostic; a composite's is the one named boundary (see the note). Measured against `52671355` (throwaway printing spec, removed again): `tuple ∘ tuple.modify(identity)` on `((1,2),(3,4))` → `((1,4),(1,4))` there, identity here; `replace(9)` → `((9,9),(9,9))` there, `((9,2),(3,9))` here; `tuple ∘ (shim ∘ shim).modify(_+1)` on `(1,2)` → `(2,2)` there, `(2,3)` here; a `fromLensF` inner NPEs there; `collectList` NPEs / MatchErrors there and works here. Upstream's own `MultiFocusFunction1Spec` is kept verbatim (14 examples / 349 expectations) and passes against this kernel; this branch's core `MultiFocusFunction1CompositionSpec` adds 13 blocks / 1003 expectations and `external/EoOpaqueSurfaceSpec` pins the public surface from outside the `eo` package tree. Gates (sbt 1.13.0, JDK 25): root `test` 237 examples / 0 failures, `scalafmtCheckAll`, `scalafixAll --check`, `scalafmtSbtCheck`, `benchmarks/scalafmtCheck`, `mimaReportBinaryIssues`, `githubWorkflowCheck`, `docs/mdoc` 0 errors, `docs/laikaSite`, `core/doc`. * build(deps): land #118 + #124 + #126 as one verified bump; fix avro's stryker blocker (#115) (#128) * build(deps): bump actions/cache from v4 to v6 Supersedes #118. Drop-in verified: action.yml inputs are byte-identical between v4.3.0 and v6.1.0 (only the node20 -> node24 runtime line differs); all 18 steps pass exactly {path, key, restore-keys}; @v6 resolves to v6.1.0, which adds graceful handling of read-only cache tokens (the accurate 'cache write denied' warning on fork PRs instead of the bogus 'another job may be creating this cache'). * build(deps): bump droste-core from 0.9.0-M3 to 0.10.0 Supersedes #124. Benchmark-only dependency (never published downstream). 0.10.0 is dependency maintenance upstream (cats update, scala-collection-compat 2.13.0, Scala.js 1.18.2); our droste surface (Fix, scheme.{cata,ana,hylo}, Algebra/Coalgebra) compiles unchanged and all nine SchemesBench benchmarks execute on the new version. * build(deps): bump sbt-stryker4s from 0.20.4 to 1.1.1 Supersedes #126. No removed or renamed features reach this build: the setting keys used in build.sbt (strykerReporters, strykerExcludedMutations, strykerThresholdsBreak) are unchanged, the borrowed-tests wiring still resolves, and 1.x requires sbt >= 1.11.2 (we run 1.13.0). Verified with a schemes smoke run (48/58 killed, 0 NoCoverage) and a full avroIntegration run (481 killed / 51 survived / 58 known-macro NoCoverage, no crash). Decisive for issue #115: 1.1.1 fixes stryker4s' rollback invariant crash ('cases should be non-empty') that killed avro's mutation run on main — 0.20.4's mutant removal could empty a Term.Match by deleting its default Pat.Wildcard case; 1.1.1 filters it. Also refreshes the invocation-doctrine comment: the module-scoped <m>/stryker form works again since 0.20.4. * fix(avro): keep AvroWalk's null-narrowing outside stryker's reach Addresses #115 blocker 1. The flow-typed 'val here = if index != null then index else ...' loses its narrowing the moment stryker4s' mutator rewrites the condition, so two mutants died as compile errors instead of being exercised (and on 0.20.4 that rollback crashed the whole run). Express the narrowing as a match on the null sentinel with an explicit JMap[String, Integer] type: a match has no condition to mutate, so the narrowing is structural rather than flow-based. (The ascribe + 'index.nn' spelling from the issue does not compile here — the guard already narrows, E216 fires, and -Werror rejects the warning.) Applied at both occurrences (totalNominalIndex and recordSlots); AvroCompileError mutants from AvroWalk go to zero. * docs: refresh version pins and the stryker invocation doctrine CLAUDE.md said Scala 3.8.3 / sbt 1.12.9; the project builds Scala 3.9.0 (build.sbt scala3Version) on sbt 1.13.0 (project/build.properties). CONTRIBUTING.md's bootstrap list pointed Scala at build.properties and omitted the JDK 25 requirement for kyo + the docs site. The 'invoke as project <m>; stryker, NOT <m>/stryker' claim stopped being true in 0.20.4 (module-scoped task resolution was fixed upstream); state the history instead of forbidding the working form. The mutationAll alias also covers zio and kyo now, not just the original eight modules. quality-assurance.md's scoverage path follows the Scala version. * build(ci): bump the workflow generator's actions/cache ref to v6 The cache bump landed as a hand-edit of the generated ci.yml (mirroring Dependabot's #118 diff), which githubWorkflowCheck rightly flags: ci.yml is generator-owned (CONTRIBUTING.md: never hand-edit it). This pairs the edit with its source of truth — UseRef.Public("actions", "cache", "v6") in the setup-java cache patch. githubWorkflowGenerate now emits zero workflow drift, so the committed ci.yml is exactly generated output, and githubWorkflowCheck passes. * docs(benchmarks): sweep 462e037 (2026-10-03) [skip ci] * feat(core) PROTOTYPE: IndexedGlass — product-indexed split/rebuild optic (full-grid composition) Standalone spike for the grate re-design (docs/research/2026-10-05-multifocus-redesign.md SS5 phase 1). Composition yields index (I, J) and context (Ctx, I => inner.Ctx): no C =:= D evidence, no index-== addressing, no SameFocus-style witness. Grate alias = Context = Unit case (not yet composition-closed — normalization is an open item). GlassSpec: 10 examples / 505 expectations covering arbitrary tabulations, 3-level associativity, type-changing composition through a non-inline generic helper, permuted/empty/Unit/NaN/signed-zero indexes, and the explicit full-grid vs diagonal semantic change. * refactor(core): retire legacy MultiFocus Grate routing * refactor(core): move fixed-index optics to Indexed and add usage examples * feat(core): distinguish Grate and PGrate and enrich Indexed examples --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
MultiFocus[Function1[X0, *]](the Grate carrier) has exactly one read that no rule of the typesystem can serve: the
Direct → MultiFocus[Function1[X0, *]]bridge's product must turn a writtenbundle
X0 => Bback into aT, and its own carrier stores onlyUnit. That read was anull.asInstanceOf[X0]sentinel guarded by a documented constant-bundle contract. It is now a real,caller-supplied index.
data.RepresentativeIndex[X0](new) — the witness. Shipped instances for the index types theGrate factories fix with a canonical value (
Int→0,Boolean→false,Unit→()), anysingleton type via
ValueOf, andRepresentativeIndex.at(i)for everything else.Function1BroadcastOpticstoresatand reads there;forgetful2multifocusFunction1nowtakes the witness.
unobservedkeeps its two remaining sites — both existential leftovers the write pathdiscards — and its docstring now states the stronger invariant: neither site is an index.
The first commit (
f7065947) is the sibling baseline change "compose the Function1 (Grate)carrier positionally" (source branch
fix/grate-positional-composition, commit81a53d3d),replayed onto
main. It is not onmainand has not been pushed anywhere; the second commitcannot compile without its
Function1BroadcastOptic/ per-index-write kernel work.If you'd rather review them separately I can push the baseline as its own branch and retarget this
PR onto it — say the word.
Behaviour: nothing user-observable changes (measured, not asserted)
Every shipped path hands that
froma constant bundle (modify/replace/collect*map theoptic's own broadcast, and the kernel's fallback passes a constant), so the witness value cannot be
observed through them. The baseline's positional guarantee is also untouched:
grate ∘ isostillrebuilds per index through
broadcastFrom, which needs no index at all.A test pins the read rule itself on a hand-built varying bundle (
-1000at index 0 vs-990atindex 1) — white-box, because outside
eothe carrier's leftoverXis abstract, so a caller cannoteven name such a bundle (
Found: Unit, Required: bridged.X). That is the honest shape of thischange: it makes an unobservable read structural rather than contract-guarded.
Costs, honestly
Composer. Call sites are unchanged forInt/Boolean/Unit/ singleton index types (instances resolve off the companion — still noimports); a grate over an algebraic index needs
given RepresentativeIndex[X] = RepresentativeIndex.at(v).Function1[Nothing, *], a phantom slot):there is no index to witness, so a read that has no answer is refused instead of forged. That is a
capacity regression in an exotic corner, and it is deliberate.
Boolean→falseinstance does privilege a value. Dropping the shippedinstances together is the stricter-consistency option: three composition cells go red and every
caller writes a
given.Relationship to #123 (please read this before deciding)
#123 retired
representableAtbecause "position is a read-time argument, never a property of theoptic" — and its write-up records that giving
repr0runtime meaning would take "a field on theoptic class plus a runtime witness match (the
Function1BroadcastOpticshape)". This PR is thatshape, so it has to be the deliberate exception, not a quiet contradiction:
.at(i)subsumes a factory's index; nothing subsumes the bridge's. Every read of a builtGrate optic is caller-driven. The bridge's
fromis the opposite: the kernel hands it a wholebundle and there is no caller to ask — real value at construction, or forged value at the read.
repr0said"this optic is at this index" (false — two calls built the same optic). The witness says "if you
ever have to read a bundle this optic did not build, read it here" — unobservable on every shipped
path.
If the line's answer is "hold #123's ruling consistently — no index on an optic, ever", then this PR
should be closed rather than merged, and the sentinel stays. Both are recorded in the design note
(§4 for the comparison, §7 for the recommendation with its cost).
Tests / evidence
Design note with the full analysis, the stand-in inventory, the measured candidate supplies
(
Representablehas no representative index;ValueOfdoes not coverInt/Boolean) and thealternative designs:
docs/research/2026-09-30-grate-witness-index.md.core/testOnly dev.constructive.eo.MultiFocusFunction1Spec— 14 examples / 349 expectations:the witness read is observable on a varying bundle; the shipped path is witness-invariant (round
trip equals identity,
modifyunaffected) for two different witnesses;grate ∘ isostayspositional under a deliberately wrong witness (
at(2):(10,20,30) → (11,21,31)); an algebraicindex bridges off a one-line
given; the witness works on refactor(core): retire MultiFocus.representableAt — therepr0index was unobservable #123's own permutedTri/Slotfixture (index order ≠ field order) with
modifystill equal to the instance'smap; anunwitnessed index does not bridge (
typeChecks); the shipped instances name real values; theBoolean-indexed cell resolves import-free.
tests/testOnly dev.constructive.eo.{GrateShapeSpec, CompositionMatrixSpec, UnlawfulFixturesSpec}— 23 / 121 / 4 examples (the
iso ∘ gratecell stays green; no cell moves).scalafmtCheckAll scalafmtSbtCheck benchmarks/scalafmtCheck githubWorkflowCheck mimaReportBinaryIssues test→ green,
scalafixAll --check→ green,sbt doc→ clean (no new scaladoc warnings),docs/mdoc→ 0 errors,docs/laikaSite→ generated.multifocus.md(bridge row + the "carries no constraint"paragraph), the generated QA grate legend (script and page kept in sync), the Grate grid's header
comment in
GrateShapeSpec,mima.sbt(0.19 break entry per the line's convention),CHANGELOG.md,and the
corerow of the agent guide.Not benchmarked, by construction:
MultiFocusCollectBenchdrivesMultiFocus.tupledirectly andnever builds a bridge, so this change cannot appear on that measured path (neither win nor loss).